package fr.loof.m3plugin;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.DirectoryScanner;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.model.Annotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaSource;
import com.thoughtworks.qdox.model.Type;

/**
 * Instruments bytecode
 *
 * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a>
 * @goal generate
 * @phase generate-sources
 * @requiresDependencyResolution compile
 */
public class GenerateMojo
    extends AbstractMojo
{
    /**
     *
     */
    private static final String JPA_ENTITY = "javax.persistence.Entity";

    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    /**
     * @parameter default-value="**\/*.java"
     */
    private String includes;

    /**
     * @parameter
     */
    private String excludes;

    /**
     * @parameter expression="${project.build.sourceEncoding}"
     */
    private String encoding;

    /**
     * @parameter expression="${project.build.directory}/generated-sources/metamodel"
     */
    private File outputDirectory;

    public void execute()
        throws MojoExecutionException
    {
        outputDirectory.mkdirs();

        JavaDocBuilder builder = createJavaDocBuilder();

        List<String> sourceRoots = project.getCompileSourceRoots();
        boolean generated = false;
        for ( String sourceRoot : sourceRoots )
        {
            File root = new File( sourceRoot );
            String[] files = scan( root ).getIncludedFiles();
            for ( String file : files )
            {
                File sourceFile = new File( root, file );
                String className = getTopLevelClassName( file );
                JavaClass clazz = builder.getClassByName( className );
                if ( !isEligible( clazz ) )
                {
                    continue;
                }
                generated = true;

                File targetFile = getTargetFile( file );
                if ( isUpToDate( sourceFile, targetFile ) )
                {
                    getLog().debug( targetFile.getAbsolutePath() + " is up to date. Generation skipped" );
                    continue;
                }
                getLog().debug( "generate MetaModel class for @Entity " + clazz.getFullyQualifiedName() );
                generateMetaModel( clazz, targetFile );
            }
        }
        if ( generated )
        {
            project.addCompileSourceRoot( outputDirectory.getAbsolutePath() );
        }
    }

    /**
     * @param clazz
     * @return
     */
    private boolean isEligible( JavaClass clazz )
    {
        for ( Annotation annotation : clazz.getAnnotations() )
        {
            String annotationFQCN = annotation.getType().getJavaClass().getFullyQualifiedName();
            if ( JPA_ENTITY.equals( annotationFQCN ) )
            {
                return true;
            }
        }
        return false;
    }

    private JavaDocBuilder createJavaDocBuilder()
        throws MojoExecutionException
    {
        try
        {
            JavaDocBuilder builder = new JavaDocBuilder();
            builder.getClassLibrary().addClassLoader( getProjectClassLoader() );
            for ( String sourceRoot : (List<String>) project.getCompileSourceRoots() )
            {
                builder.addSourceTree( new File( sourceRoot ) );
            }
            return builder;
        }
        catch ( MalformedURLException e )
        {
            throw new MojoExecutionException( "Failed to resolve project classpath", e );
        }
        catch ( DependencyResolutionRequiredException e )
        {
            throw new MojoExecutionException( "Failed to resolve project classpath", e );
        }
    }

    private ClassLoader getProjectClassLoader()
        throws DependencyResolutionRequiredException, MalformedURLException
    {
        List<?> compile = project.getCompileClasspathElements();
        URL[] urls = new URL[compile.size()];
        int i = 0;
        for ( Object object : compile )
        {
            if ( object instanceof Artifact )
            {
                urls[i] = ( (Artifact) object ).getFile().toURI().toURL();
            }
            else
            {
                urls[i] = new File( (String) object ).toURI().toURL();
            }
            i++;
        }
        return new URLClassLoader( urls, ClassLoader.getSystemClassLoader() );
    }

    private File getTargetFile( String source )
    {
        String targetFileName = source.substring( 0, source.length() - 5 ) + "_.java";
        File targetFile = new File( outputDirectory, targetFileName );
        targetFile.getParentFile().mkdirs();

        return targetFile;
    }

    private String getTopLevelClassName( String sourceFile )
    {
        String className = sourceFile.substring( 0, sourceFile.length() - 5 ); // strip ".java"
        return className.replace( File.separatorChar, '.' );
    }

    private boolean isUpToDate( File sourceFile, File targetFile )
    {
        return targetFile.exists() && targetFile.lastModified() > sourceFile.lastModified();
    }

    private DirectoryScanner scan( File sourceRoot )
    {
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir( sourceRoot );
        scanner.setIncludes( includes.split( "," ) );

        if ( excludes != null )
        {
            scanner.setExcludes( excludes.split( "," ) );
        }
        scanner.scan();
        return scanner;
    }

    protected void generateMetaModel( JavaClass clazz, File targetFile )
        throws MojoExecutionException
    {
        try
        {
            PrintWriter writer =
                ( encoding != null ? new PrintWriter( targetFile, encoding ) : new PrintWriter( targetFile ) );

            String className = clazz.getName();
            JavaSource javaSource = clazz.getSource();
            writer.println( "package " + javaSource.getPackage().getName() + ";" );
            writer.println();
            writer.println( "import javax.jpa.metamodel.Metamodel;" );
            writer.println( "import javax.persistence.metamodel.CollectionAttribute;" );
            writer.println( "import javax.persistence.metamodel.ListAttribute;" );
            writer.println( "import javax.persistence.metamodel.MapAttribute;" );
            writer.println( "import javax.persistence.metamodel.SetAttribute;" );
            writer.println( "import javax.persistence.metamodel.SingularAttribute;" );

            writer.println();
            writer.println( "@Metamodel" );
            writer.println( "public class " + className + "_" );
            writer.println( "{" );

            JavaField[] fields = clazz.getFields();
            for ( JavaField field : fields )
            {
                if ( field.isTransient() || field.isStatic() )
                {
                    continue;
                }

                writer.println( "" );
                writer.print( "    public static volatile " );
                Type type = field.getType();
                if ( isMap( type ) )
                {
                    String generic = getGenerics( type );
                    writer.print( "MapAttribute<" + className + ", " + generic + "> " );
                }
                else if ( isSet( type ) )
                {
                    String generic = getGenerics( type );
                    writer.print( "SetAttribute<" + className + ", " + generic + "> " );
                }
                else if ( isList( type ) )
                {
                    String generic = getGenerics( type );
                    writer.print( "ListAttribute<" + className + ", " + generic + "> " );
                }
                else if ( isCollection( type ) )
                {
                    String generic = getGenerics( type );
                    writer.print( "CollectionAttribute<" + className + ", " + generic + "> " );
                }
                else
                {
                    writer.print( "SingularAttribute<" + className + ", " + getType( type ) + "> " );
                }
                // TODO PluralAttribute ?
                writer.print( field.getName() + ";" );
                writer.println();
            }
            writer.println();
            writer.println( "}" );
            writer.close();
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "Failed to create MetaModel file " + targetFile, e );
        }
    }

    private String getType( Type type )
    {
        if ( type.isPrimitive() )
        {
            return WRAPPERS.get( type.getJavaClass().getName() );
        }
        return type.getJavaClass().getName();
    }

    private String getGenerics( Type type )
    {
        String generic = type.getGenericValue();
        return generic.substring( generic.indexOf( '<' ) + 1, generic.lastIndexOf( '>' ) );
    }

    /**
     * @param type
     * @return
     */
    private boolean isCollection( Type type )
    {
        return type.getJavaClass().isA( "java.util.Collection" );
    }

    private boolean isMap( Type type )
    {
        return type.getJavaClass().isA( "java.util.Map" );
    }

    private boolean isSet( Type type )
    {
        return type.getJavaClass().isA( "java.util.Set" );
    }

    private boolean isList( Type type )
    {
        return type.getJavaClass().isA( "java.util.List" );
    }

    private final static Map<String, String> WRAPPERS = new HashMap<String, String>();
    static
    {
        WRAPPERS.put( "boolean", "Boolean" );
        WRAPPERS.put( "byte", "Byte" );
        WRAPPERS.put( "char", "Character" );
        WRAPPERS.put( "short", "Short" );
        WRAPPERS.put( "int", "Integer" );
        WRAPPERS.put( "long", "Long" );
        WRAPPERS.put( "float", "Float" );
        WRAPPERS.put( "double", "Double" );
    }
}
